﻿using System;
using System.Collections.Generic;
using System.Threading;
using LoungeRepo.Contracts.EntityCache;

namespace LoungeRepo.EntityCache
{
    public partial class SecondLevelCache : IEntityCache
    {
        enum LockModes
        {
            LockForRead,
            LockForWrite
        }


        private readonly Dictionary<string, object> keyIndex = new Dictionary<string, object>();
        private readonly Dictionary<Type, Dictionary<object, object>> typeIndexes = new Dictionary<Type, Dictionary<object, object>>();

        private readonly ReaderWriterLock rwLock = new ReaderWriterLock();


        public void Register(object entity, string id, string partition)
        {
            //TODO: should register overwrite an existing cache entry with the same key?
            var entityKey = CreateEntityKey(id, partition);

            this.rwLock.AcquireReaderLock(500);
            if (!this.keyIndex.ContainsKey(entityKey))
            {               
                this.rwLock.UpgradeToWriterLock(500); // only acquire writer lock if really necessary
                try
                {
                    this.keyIndex.Add(entityKey, entity);

                    Dictionary<object, object> entityTypeIndex;
                    if (!this.typeIndexes.TryGetValue(entity.GetType(), out entityTypeIndex))
                    {
                        entityTypeIndex = new Dictionary<object, object>();
                        this.typeIndexes.Add(entity.GetType(), entityTypeIndex);
                    }

                    entityTypeIndex.Add(entity, entity);
                }
                finally
                {
                    this.rwLock.ReleaseWriterLock();
                }
            }
            else
                this.rwLock.ReleaseReaderLock();
        }


        public void Evict(string id, string partition)
        {
            Lock(() =>
                     {
                         var entityKey = CreateEntityKey(id, partition);
                         object entity;
                         if (this.keyIndex.TryGetValue(entityKey, out entity))
                         {
                             this.typeIndexes[entity.GetType()].Remove(entity);
                             this.keyIndex.Remove(entityKey);
                         }
                     },
                 LockModes.LockForWrite
                );
        }


        public IEnumerable<T> GetByType<T>()
        {
            return Lock<IEnumerable<T>>(() =>
                        {
                            if (this.typeIndexes.ContainsKey(typeof (T)))
                            {
                                List<T> entities = new List<T>();
                                foreach (object entity in this.typeIndexes[typeof (T)].Values)
                                    entities.Add((T) entity);
                                return entities;
                            }

                            return new List<T>();
                        }, 
                        LockModes.LockForRead
                   );
        }


        public bool TryGetByKey(string id, string partition, out object entity)
        {
            object localEntity = null;
            var success = Lock(() =>
                                  {
                                      var entityKey = CreateEntityKey(id, partition);
                                      return this.keyIndex.TryGetValue(entityKey, out localEntity);
                                  },
                              LockModes.LockForRead
                );
            entity = localEntity; // needed, because lambda func cannot assign to out params
            return success;
        }


        private string CreateEntityKey(string id, string partition)
        {
            return partition + "/" + id;
        }
    }
}
